import base64
import contextlib
import json
import os
import threading
import traceback
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from logger import get_main_logger,get_student_logger
import config
import requests
import urllib3
import wechatpy
from Crypto.Cipher import AES
from flask import Flask, request, abort, Response, jsonify
from wechatpy import parse_message
from wechatpy.crypto import WeChatCrypto
from wechatpy.exceptions import InvalidAppIdException
from wechatpy.exceptions import InvalidSignatureException
from wechatpy.replies import create_reply
from wechatpy.utils import check_signature

import empty_classrooms
from Jwgl import JwglManager, JwglAutoLoginer
from models import db, Code, CourseData
from parseCourseData import parseCourseData, getStuInfo

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app_config = config.app_config

app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
db.create_all(app=app)

secret = config.secret
wechatConfig = config.wechatConfig

client = wechatpy.WeChatClient(wechatConfig["app_id"], wechatConfig["secret"])
if os.getenv("http_proxy"):
    proxies = {
        "http": os.getenv('http_proxy'),
        "https": os.getenv('https_proxy'),
    }
    client._http.proxies.update(proxies)


def decrypt(raw):
    obj = AES.new(secret.encode(), AES.MODE_CBC, secret[:16].encode())
    d = obj.decrypt(base64.urlsafe_b64decode(raw)).lstrip(b' ')
    return json.loads(d)


def send_message(source, content, raise_e=False):
    try:
        r = client.message.send_text(source, content)
        if r["errcode"] != 0:
            print(source, r)
    except wechatpy.exceptions.WeChatClientException as e:
        print(e)
    except Exception as e:
        if raise_e:
            raise e
        print(e)


@contextlib.contextmanager
def write_user_log(username, filename, mode='w'):
    f = Path(__file__).parent / 'log_files' / str(username)
    f.mkdir(exist_ok=True)
    with open(f / filename, mode=mode, encoding='utf-8' if mode == 'w' else None) as f2:
        yield f2


error_msg = "遇到问题，加入iBeiKe校园交流QQ群：963310300。"
server_base_url = "https://jwgl-api.ibeike.work"


# 接受客服消息
# https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/receive.html
@app.route("/wechat", methods=["GET", "POST"])
def wechat():
    signature = request.args.get("signature", "")
    timestamp = request.args.get("timestamp", "")
    nonce = request.args.get("nonce", "")
    echo_str = request.args.get("echostr", "")
    msg_signature = request.args.get("msg_signature", "")

    try:
        check_signature(wechatConfig["token"], signature, timestamp, nonce)
    except InvalidSignatureException:
        abort(403)
    if request.method == "GET":
        return echo_str
    else:
        crypto = WeChatCrypto(wechatConfig["token"], wechatConfig["aes"], wechatConfig["app_id"])
        try:
            msg = crypto.decrypt_message(request.data, msg_signature, timestamp, nonce)
        except (InvalidSignatureException, InvalidAppIdException):
            abort(403)
        msg = parse_message(msg)
        if msg.type != 'event':
            code = Code(msg.source)
            db.session.add(code)
            db.session.commit()

            jal = JwglAutoLoginer()
            lck, entityId = jal.get_rand_entityId()
            mic = jal.authn_getMic(entityId, lck)
            sid = jal.get_sid(mic["data"]["appId"], mic["data"]["returnUrl"], mic["data"]["randomToken"])

            jal_data = base64.b64encode(json.dumps({
                "glht_logon_url": jal.glht_logon_url,
                "sso_return_url": jal.sso_return_url
            }).encode()).decode()
            mic_data = base64.b64encode(json.dumps(mic).encode()).decode()
            qrcode_url = f'https://sis.ustb.edu.cn/connect/qrimg?sid={sid}'
            msg1 = f"第一步: <a href='{qrcode_url}'>点击此处扫码登录</a>"
            send_message(msg.source, msg1)
            confirm_url = f"{server_base_url}/qrcode_confirm?sid={sid}&openid={msg.source}&jal={jal_data}&mic={mic_data}"
            msg2 = f"第二步: <a href='{confirm_url}'>点击此处更新课表</a>"
            send_message(msg.source, msg2)

    return crypto.encrypt_message(create_reply("", msg).render(), nonce, timestamp)


@app.route("/qrcode_confirm")
def qrcode_confirm():
    sid = request.args.get("sid")
    open_id = request.args.get("openid")
    jal_data = json.loads(base64.b64decode(request.args.get("jal")).decode())
    mic = json.loads(base64.b64decode(request.args.get("mic")).decode())

    jal = JwglAutoLoginer()
    jal.glht_logon_url = jal_data["glht_logon_url"]
    jal.sso_return_url = jal_data["sso_return_url"]

    ok, auth_code = jal.connect_state(sid)
    if not ok:
        return f"<h1>扫码失败: 请重新扫码登录</h1>"

    url = f'{mic["data"]["returnUrl"]}&appid={mic["data"]["appId"]}&auth_code={auth_code}&rand_token={mic["data"]["randomToken"]}'
    res = requests.get(url)
    login_code = jal.get_login_code(res.text)

    session = requests.Session()
    session.get(jal.glht_logon_url + login_code)
    cookies = jal.get_cookie_str(session)
    jm = JwglManager(session)

    err_msg = "可加入iBeiKeQQ群: 963310300, 并提供学号以便排查"
    if not jm.check_login():
        return f"<h1>登录失败, {err_msg}</h1>"

    stuInfo = getStuInfo(cookies=cookies)
    stu_id = stuInfo.stu_id
    course_list = stuInfo.course_list
    
    # if stu_id == config.STU_ID:
    #     stu_logger=get_student_logger(stu_id)
    #     stu_logger.error(f"学号{stu_id}数据解析数据如下: {course_list}")
    
    try:
        info = parseCourseData(cookies=cookies)
    except Exception as E:
        # 保存报错信息
        stu_logger=get_student_logger(stu_id)
        stu_logger.error(f"学号{stu_id}数据解析可能出错，错误数据如下{course_list}\n\nerror_info:{E}")
        return f"<h1>数据更新发生错误, {err_msg}</h1>"

    courseList = info.result

    # send_message(open_id, stu_id)
    try:
        course_data = CourseData(open_id, json.dumps(courseList), stu_id)
        db.session.merge(course_data)
        db.session.commit()
    except Exception as E:
        stu_logger = get_student_logger(stu_id)
        stu_logger.error(f"学号{stu_id}的数据更新可能出错，错误数据如下：{course_data}\n\nerror_info:{E}")
        return f"<h1>数据更新发生错误, {err_msg}</h1>"

    send_message(open_id, '数据更新已完成，请返回小程序点击“我已完成数据更新”')
    return Response('<h1>数据更新已完成，请返回小程序点击“我已完成数据更新”</h1>')


# 课表后端 php 回调 ， 本科生和研究生的逻辑是一样的
@app.route("/master/get_data", methods=["POST"])
@app.route("/get_data", methods=["POST"])
def get_data():
    obj: CourseData = CourseData.query.filter_by(open_id=request.form["open_id"]).first()
    if not obj:
        return jsonify({"status": -1})
    r = jsonify({"status": 0, "update_time": int(datetime.timestamp(obj.create_time)), "data": json.loads(obj.data),
                 "stu_id": obj.stu_id, "term": app_config["学期"]})
    db.session.delete(obj)
    db.session.commit()
    return r


# q:该项目哪里有sql注入的漏洞？请告诉我位置
# a:这里
# q:为什么这里有sql注入的漏洞？
# a:因为这里的参数是从前端传过来的，而前端的参数是可以被用户修改的，所以这里就有sql注入的漏洞
# q:如何解决？
# a:使用sqlalchemy的参数化查询，或者使用orm的方式查询


# 某个时间点无课教室信息
@app.route("/rest_rooms")
def rest_rooms():
    buildings = empty_classrooms.white_buildings
    a, b = int(request.args.get('a')), int(request.args.get('b'))
    day_text = request.args.get('date')
    data = db.engine.execute(
        "select building, classroom from rest_rooms where day = %s and num in (" + ','.join(
            ['%s' for _ in range(a, b + 1)]) + ") group by building, classroom having count(1) = %s",
        (day_text, *range(a, b + 1), b - a + 1)
    )

    result = []
    building_data = defaultdict(lambda: defaultdict(list))
    for i in data:
        # 如果开头带字幕 需要过滤到 比如 S301 的楼层应该是3 而不是S3
        tmp_classroom = i['classroom'][1:] if i['classroom'][0].isalpha() else i['classroom']
        building_data[i['building']][tmp_classroom[:1 if len(tmp_classroom) == 3 else 2]].append(i['classroom'])

    for building in building_data:
        result.append({
            'name': building,
            'data': sorted(
                [{'floor': str(index), 'classroom': sorted(value)} for index, value in building_data[building].items()],
                key=lambda x: int(x['floor'])),
        })
    result.sort(key=lambda x: buildings.index(x['name']))
    return jsonify({"status": 0, "a": a, "b": b, "time": day_text, "data": result})


# 所有教室信息
@app.route("/room_list")
def room_list():
    day_text = request.args.get('date')
    data = db.engine.execute("select building, classroom from rest_rooms where day = %s group by building, classroom",
                             (day_text,))
    result = []
    building_data = defaultdict(lambda: defaultdict(list))
    for i in data:
        # 如果开头带字幕 需要过滤到 比如 S301 的楼层应该是3 而不是S3
        tmp_classroom = i['classroom'][1:] if i['classroom'][0].isalpha() else i['classroom']
        building_data[i['building']][tmp_classroom[:1 if len(tmp_classroom) == 3 else 2]].append(i['classroom'])

    for building in building_data:
        result.append({
            'name': building,
            'data': sorted(
                [{'floor': str(index), 'classroom': sorted(value)} for index, value in building_data[building].items()],
                key=lambda x: int(x['floor'])),
        })
    result.sort(key=lambda x: empty_classrooms.white_buildings.index(x['name']))
    return jsonify({"status": 0, "time": day_text, "data": result})


# 某个教室一天的课程安排
@app.route("/single_rest_room")
def single_rest_room():
    day_text = request.args.get('date')
    building = request.args.get('building')
    classroom = request.args.get('classroom')
    data = db.engine.execute(
        "select num from rest_rooms where day = %s and building = %s and classroom = %s",
        (day_text, building, classroom)
    ).fetchall()
    result = []
    for i in data:
        result.append(i[0])

    return jsonify({"status": 0, "time": day_text, "data": result})


# 课表首页的广告信息控制显示
@app.route('/getad')
def getAdvertisement():
    return jsonify({
        # 是否显示广告 ,1 表示显示，0表示不显示
        "showAD": 1,
        # 显示广告的标题信息 ，如果需要换行，直接写 \n 即可
        "adText": "课表更新遇到问题？戳我",
        # 显示广告的URL链接，经测试，不要再url中携带参数 （？，&），否则小程序端会显示参数错误
        "adURL": "https://mp.weixin.qq.com/s/gHjjqSo3V0rnhvQp9dhZhg",
    })


from dynamicCourses import DBManagement


# https://jwgl-api.ibeike.work/dynamic-courses?week=0&order=0&day=2&budding=逸夫楼
# 查询指定时间点指定楼宇正在进行的课程
@app.route('/dynamic-courses', methods=['GET'])
def dynamic_courses():
    if not request.method == 'GET':
        return "请求方式不正确"
    flag, msg = DBManagement(
        course_term=app_config["学期"],
    ).getClassDynamicInformation(
        WEEK=request.args.get("week", "10"),  # 默认给予错误的参数
        DAY=request.args.get("day", "10"),
        ORDER=request.args.get("order", "10"),
        budding=request.args.get("budding", ""),
        refresh=bool(request.args.get("refresh", False))
    )
    if flag:
        return jsonify({"code": 200, "data": msg, "msg": "ok"})
    return jsonify({"code": 500, "msg": msg, "data": []})


threading.Thread(target=empty_classrooms.main, daemon=True).start()

if __name__ == '__main__':
    app.run(port=60021)
